#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0
#
# An utility to compare two .config files and print the results in table style.
#

import sys
import argparse
import traceback

def args_parser():
    comment = ("An utility to compare two .config files and "
               "print the results in table style.")
    parser = argparse.ArgumentParser(description = comment,
                                     formatter_class =
                                         argparse.RawTextHelpFormatter)
    parser.add_argument(dest = "old_file", nargs = "?",
                        metavar = "old-file",
                        default = ".config.old",
                        help = "specify old .config file "
                               "(default: .config.old)")
    parser.add_argument(dest = "new_file", nargs = "?",
                        metavar = "new-file",
                        default = ".config",
                        help = "specify new .config file "
                               "(default: .config)")
    parser.add_argument("-S", dest = "S", action = "store_true",
                        help = "print configs that exist in both files "
                               "and are equal")
    parser.add_argument("-C", dest = "C", action = "store_true",
                        help = "print configs that exist in both files "
                               "but are not equal")
    parser.add_argument("-O", dest = "O", action = "store_true",
                        help = "print configs that only exist in old-file")
    parser.add_argument("-N", dest = "N", action = "store_true",
                        help = "print configs that only exist in new-file")
    parser.add_argument("-D", dest = "D", action = "store_true",
                        help = "a combination of -C -O -N, it's the default mode")
    parser.add_argument("-y", dest = "y", action = "store_true",
                        help = "print configs that are y")
    parser.add_argument("-n", dest = "n", action = "store_true",
                        help = "print configs that are n (not set)")
    parser.add_argument("-m", dest = "m", action = "store_true",
                        help = "print configs that are m")
    parser.add_argument("-v", dest = "v", action = "store_true",
                        help = "print configs that are "
                               "string/hex/int value")
    parser.add_argument("--old", dest = "old", action = "store_true",
                        help = "filter configs base on old-file")
    parser.add_argument("--new", dest = "new", action = "store_true",
                        help = "filter configs base on new-file")
    parser.add_argument("-p", "--prefix", dest = "prefix",
                        action = "store", default = "CONFIG_",
                        help = "config name prefix (default: CONFIG_)")
    return parser

def usage():
    args_parser().parse_args(["-h"])

def parse_args():
    args = args_parser().parse_args()
    setattr(args, "doptions", diff_options(args))
    setattr(args, "voptions", value_options(args))
    old = args.old or not args.new
    new = args.new or not args.old
    args.old = old
    args.new = new
    return args

def diff_options(args):
    doptions = []
    if args.S: doptions.append("S")
    if args.C or args.D: doptions.append("C")
    if args.O or args.D: doptions.append("O")
    if args.N or args.D: doptions.append("N")
    if len(doptions) == 0:
        doptions = ["C", "O", "N"]
    return doptions

def value_options(args):
    voptions = set()
    if args.y: voptions.add("y")
    if args.n: voptions.add("n")
    if args.m: voptions.add("m")
    if args.v: voptions.add("v")
    if len(voptions) == 0:
        voptions = {"y", "n", "m", "v"}
    return voptions

def test_value(val, voptions):
    if val is None: return False
    if val in voptions: return True
    return (not val in {"y", "n", "m"}) and ("v" in voptions)

def format_exception():
    es = ""
    exc_type, exc_value, exc_traceback = sys.exc_info()
    exc_str = traceback.format_exception(exc_type, exc_value, exc_traceback)
    for s in exc_str:
        es += s
    return es

def read_line(line, prefix):
    line = line.strip()
    if line.endswith(" is not set"):
        beg = line.find(prefix)
        if beg == -1: return None, None
        name, val = line[beg:-10].rsplit(" ", 1)
        return name.strip(), "n"
    if line.startswith(prefix):
        if line.find("=") == -1: return None, None
        name, val = line.split("=", 1)
        return name.strip(), val.strip()
    return None, None

def read_file(filename, prefix):
    configs = {}
    with open(filename, "r", encoding = "utf-8") as f:
        for line in f:
            name, val = read_line(line, prefix)
            if not name is None: configs[name] = val
    return configs

def compare_config(args):
    result = {"S": {}, "C": {}, "O": {}, "N": {}}
    try:
        old_configs = read_file(args.old_file, args.prefix)
        new_configs = read_file(args.new_file, args.prefix)
        while len(old_configs) > 0:
            name, old_val = old_configs.popitem()
            new_val = new_configs.pop(name, None)
            if new_val is None:
                result["O"][name] = (old_val, None)
            elif old_val == new_val:
                result["S"][name] = (old_val, new_val)
            else:
                result["C"][name] = (old_val, new_val)
        while len(new_configs) > 0:
            name, new_val = new_configs.popitem()
            result["N"][name] = (None, new_val)
    except:
        print(format_exception())
        usage()
    return result

def filter_output(result, args):
    output = {"S": {}, "C": {}, "O": {}, "N": {}}
    for opt in args.doptions:
        for name, val in result[opt].items():
            if (args.old and test_value(val[0], args.voptions) or
                args.new and test_value(val[1], args.voptions)):
                old_val = "-" if val[0] is None else val[0]
                new_val = "-" if val[1] is None else val[1]
                output[opt][name] = (old_val, new_val)
    return output

def print_table(output, args):
    name_max_len = 8
    old_max_len  = 8
    new_max_len  = 8
    name_list = sum([list(output[opt].keys()) for opt in args.doptions], [])
    if len(name_list) > 0:
        name_max_len = len(max(name_list, key = len))
    val_list = sum([list(output[opt].values()) for opt in args.doptions], [])
    if len(val_list) > 0:
        old_max_len = len(max([val[0] for val in val_list], key = len))
        new_max_len = len(max([val[1] for val in val_list], key = len))
    diff_max_len = len(max([diff_types[opt] for opt in args.doptions], key = len))
    header = ["NAME", "DIFF", "OLD", "NEW"]
    # table row format
    row = ("{{:{}}}\t{{:{}}}\t{{:{}}}\t{{:{}}}"
           .format(min(max(name_max_len, len(header[0])), 40),
                   min(max(diff_max_len, len(header[1])), 40),
                   min(max(old_max_len,  len(header[2])), 40),
                   min(max(new_max_len,  len(header[3])), 40)))
    print(row.format(header[0], header[1], header[2], header[3]))
    for opt in args.doptions:
        for name, val in sorted(output[opt].items()):
            print(row.format(name, diff_types[opt], val[0], val[1]))

def print_summary(output, args):
    diff_max_len = len(max([diff_types[opt] for opt in args.doptions], key = len))
    # summary line format
    line = "{{:{}}}: {{}}".format(max(diff_max_len, 8))
    print("\nSummary:")
    print(line.format("Old-file", args.old_file))
    print(line.format("New-file", args.new_file))
    total = 0
    for opt in args.doptions:
        count = len(output[opt])
        print(line.format(diff_types[opt], count))
        total += count
    print(line.format("Total", total))

def print_result(result, args):
    output = filter_output(result, args)
    print_table(output, args)
    print_summary(output, args)

diff_types = {"S": "Same", "C": "Changed", "O": "Old-only", "N": "New-only"}
args = parse_args()
result = compare_config(args)
print_result(result, args)
